Skip to content

fix(metadata-protocol)!: a package-less row at a holder's exact address answers one thing on every read and in every hydration state: declined for a sealed type, refused with two holders for an overlay type (ADR-0150 D4) - #32741

Merged
objectstack-fleet[bot] merged 69 commits into
mainfrom
claude/project-thread-pjmygu
Nov 13, 2026

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread
Fixes #32568

Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/** and content/docs/** teach no exact, draft or layered read of a package-less row of a name a package holds; the prescription ships in the refusal text and the FROM → TO below.

维护者速读

  • 改了什么:不属于任何包的元数据行(package-less row)在某个持有包的精确地址(?package=X)上,按类型类别只给一种答案:可覆盖类型(email_template、dashboard、report)两个包同名时,草稿读、预览、分层读的 draft 层也一律 409 列出全部持有者;封闭类型(action、queue、hook 等)一律不采用这行,返回持有包自己的条目;读侧回填不再把这行塞进「第一个注册的包」的槽位。
  • 为什么改:ADR-0150 D4 与 ADR-0126 Regime C,按专题裁决 6817768147 落实:同一行数据在回填前后、草稿与正式读之间答案不同,属于「歧义被猜而不是被报告」(专题红线 2)。
  • 风险与回滚:只改读路径,不改存储与 schema;第一方组合里没有两个包同名又带无包行的情况,计数为 0。回滚即 revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-registry-hydration.ts(回填不再借第一个包的外壳)、packages/metadata-protocol/src/protocol-meta-item-reads.ts(草稿与预览出口)、packages/metadata-protocol/src/protocol-exact-qualifier.ts(declinedCopyReplaced,平读与分层读的 code 层都先过它)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/package-scoped-exact-qualifier.test.ts,预期全部通过,其中 [#32568] 一组覆盖四种状态 × 两种注册顺序。
  • 需要你做的:无,席位落地。

What changes

A package-less row (a sys_metadata row bound to no package, or its copy in the registry's bare slot after a hydration) met at a holder's exact address now answers one thing on every read and in every hydration state, by type class (epic ruling 6817768147 on this card; ADR-0150 D4, ADR-0126 Regime C):

  • Overlay types (email_template, dashboard, report): the row is a holder's customization. Two holders: 409 RESOURCE_CONFLICT, ambiguous-in-closure, naming every holder, on the strict draft read, the preview and the layered draft layer as on the active read; the list scoped to a holder no longer previews such a draft. One holder: served, as before.
  • Sealed managed types (artifact-backed, no overlay channel): the row is declined on every read at the address (active and its registry or service copy, strict draft, preview, layered read with its code layer, layered draft layer). The address serves the holder's own shipped item, with one holder or two. The bare read of a two-holder name keeps its 409.
  • The read-side hydrator (hydrateOverlayIntoRegistry) never borrows an envelope through a package-less lookupArtifactItem: a package-less row wears its one holder's envelope, none with two holders, and a sealed type's declined row is not registered at all. A copy registered before the package arrived is healed by the one heal walk.
  • Upgrade guide: semantic entry package-less-row-exact-address-by-type-class (packages/spec/src/migrations/entries/semantic/18.package-less-row-exact-address-by-type-class.ts), registered by the changeset (adr-0087: registered), with its own @objectstack/spec patch changeset. The remedy for a sealed type turns on whether it has a declared override channel (ADR-0146): flow and action (under D9's conditions), page, app, dataset, doc, book and mapping move the change to a declared override in the holder's .custom package; every other sealed type (hook, job, queue, sharing_rule, assignment_rule, agent, skill, api, …) has none, so the row is deleted and the change ships in a new version of the holding package, or lives under a name no package ships.

Branch note (Duplicate Fix Guard): this branch, claude/project-thread-pjmygu, continues claude/project-thread-cyidlz@191cd2fab (the previous holder's pushed WIP for this card) with origin/main merged in, never rebased; claude/project-thread-cyidlz is left as it was.

Merges with main after the ACCEPT on a5a3a66a9 (no rebase):

FROM → TO

# Read (at ?package=X unless named) FROM TO Clause-② arm
1 Overlay type, two holders, package-less draft: strict draft read, preview, layered draft layer 200, the package-less draft at both holders 409 ambiguous-in-closure naming both narrowing
2 Overlay type, two holders: list scoped to a holder with drafts previewed previews the package-less draft the holder's own item narrowing
3 Sealed type (action), package-less draft, one holder or two: strict draft read / preview / layered draft layer 200 the draft / the draft / the draft 404 no draft / the holder's own active item / null narrowing
4 Sealed type, two holders, after the read-side hydrator (state 2) 200, the stored body served as the first-registered holder's own 200, each holder's own narrowing
5 Sealed type, two holders, bare-slot copy stamped '' or sys_metadata (state 3) action 409 naming both; queue the copy 200, each holder's own widening for action with two holders; narrowing for queue
5b agent, skill, app: the environment's item, or a package-less registry copy stamped '' or sys_metadata, of a name a package ships (the [#31699] pins, which now hold the overlay types dashboard and report only) two holders: 409 naming both; one holder: the item each holder's own widening with two holders; narrowing with one
6 Sealed type, one holder or two: layered read ?layers=true two holders: action 409 naming both, queue reported the row as each holder's overlay; one holder: the row reported as the holder's overlay overlay: null, the holder's own as effective widening for action with two holders; narrowing otherwise
7 Sealed type, one holder, after the read-side hydrator: exact read and bare read (the one-holder flip) the stored body the holder's own item, before and after narrowing
8 hook, job, queue, sharing_rule, assignment_rule: environment item of a name two packages hold (location 2) the environment's item at both holders each holder's own narrowing
9 action, one holder: the environment's item (the #32407 control) the environment's item the holder's own action narrowing
10 Registry-direct read of a sealed app / page with a package-less stored row (what nav code reads) the stored body under the package's envelope the package's own definition narrowing
11 The bare /actions/:object/NAME route (names no package) on an object neither of two holders binds, with the environment's package-less action bound there the environment's item, found through the first holder's exact address (?package=<first> served it, stamped as that holder's, so the first holder's own route on its own object answered no declaration; the second holder's served its own) no declaration (not a refusal); the execution face reading the environment's own item is #32751 narrowing
12 Sealed type (action, queue), one holder or two: layered read's code layer when the metadata service answers a package-less copy of the declined row one holder: the copy as the holder's code and effective; two holders: action 409 naming both, queue the copy the holder's own as code and effective, as the plain read already answered widening for action with two holders; narrowing otherwise
13 Sealed type (agent; action on the plain and the layered read; queue; the other sealed types move the same way, as rows 5 and 8 cover them), two holders, one of them disabled (the served holder set of #32497), then re-enabled main with #32497: the enabled holder's address served the environment's item (the layered read reported a stored row as its overlay); re-enabled, 409 naming both the holder's own item (overlay: null), disabled or re-enabled; the disabled holder's address still answers its withheld item narrowing while one is disabled; widening for re-enabled action and agent (as row 5)

Unchanged, pinned as CONTROLs: the bare read of a two-holder action (409, after the hydrator too); an overlay type's package-less row with one holder (served on every exit, and the hydrator lays it in under that holder's envelope); email_template with two holders (409 naming both, before and after the hydrator, and the first holder's slot keeps its own).

The widening in rows 5, 5b, 6 and 12 (action, and agent, skill, app, with two holders: 409 → 200 the holder's own item) rests on the epic ruling on this card, 6817768147: with the row declined, ?package=X serves X's own shipped item, exactly as it would with no row stored. The same declining moves an answer on the dataset query door (POST /analytics/dataset/query and its batch door), which reads each holder at its exact address (#32804): with a package-less dataset row beside two holders, each holder's exact read no longer refuses, so #32804's "every holder's own read refuses → 503 for every caller" no longer fires for that composition. The bare, ?callerPackage= and non-holder ?package= arms move from 503 to the admission-judged answer (404 admitted to neither; 200 running the one admitted holder; 409 naming only the admitted holders), and ?package=<holder> moves from 503 to that holder's own dataset (widening). The package-less row never runs. Measured on 023ace703 with #32804's rig: alpha admitted only → bare and ?callerPackage= 200 alpha_item, ?package=<non-holder> 409 naming alpha alone; neither admitted → 404; both → 409. No @objectstack/rest product code changes, so no rest changeset.

No export is added: declinesPackageLessRowAt, isPackageLessRow and declinedCopyReplaced are module-internal (not in src/index.ts).

Tests

  • packages/objectql/src/package-scoped-exact-qualifier.test.ts: a new [#32568] describe. Locations 1 and 3 (draft exits, overlay two holders refused, one-holder CONTROL, sealed declined with one holder and two); location 2 (the five types); location 4 for action and queue in four states (not hydrated, after the read-side hydrator, bare-slot copy stamped '', stamped sys_metadata) × both registration orders, the one-holder flip, the bare-read CONTROL, and the overlay CONTROL. The [#31699] and [#32407] pins whose answers this moves are rewritten in place with the old answer named in the title.
  • protocol-boot-hydration-scoped.test.ts, protocol-registry-shadow.test.ts: the legacy first-match graft pins now pin that a sealed type's package-less row is not registered over the package's item.
  • packages/objectql/src/package-scoped-exact-qualifier.test.ts (contract review S1): a metadata service answering a package-less copy, for action and queue with one holder and two: the plain read and the layered read's code layer both answer the holder's own. All four failed before the layered fix (the code layer answered stored, or 409 for action with two holders) and pass after it.
  • packages/runtime/src/action-package-addressing.test.ts: the describe's docblock now states the route reads each holder's exact address, where the package-less copy is declined (runtime action route: an environment action on an object neither of two holders binds cannot run through the bare route once #32568 lands #32751 for the execution face).
  • protocol.lookup-artifact-item-call-sites.test.ts: the census rows for the hydrator's lookup and the new declinedCopyReplaced lookup.
  • packages/rest/src/analytics-routes.test.ts (merge queue, after fix(rest): the dataset query door judges admission on the refusal arm before it names holders (ADR-0150 D3 after ADR-0149 D2) #32804): fix(rest): the dataset query door judges admission on the refusal arm before it names holders (ADR-0150 D3 after ADR-0149 D2) #32804's two 503 cases (a holder whose own exact-qualifier read refuses…, ?package= naming a holder whose own address refuses…) made each holder's exact read refuse by registering a package-less dataset row beside both holders. dataset is a sealed type, so that row is now declined at each holder's address and the read serves the holder's own (the queue run read 404 and 200 where 503 was pinned). The cases now throw that read's former refusal (ambiguous-in-closure naming both holders) from the holder's exact read, so the 503 they pin is unchanged; a new [#32568] pin holds the declined row: ?package= runs the holder's own dataset. Ablation: without the rig both cases fail, 2 failed / 55 passed.

Acceptance notes

Verification

Merge-queue round on head 023ace703 (merges main 5c0f11349 at 176315702, no textual conflict; 023ace703 changes packages/rest/src/analytics-routes.test.ts only):

Measured on head 533a4f932 (merges main 2f80f2897), after the dogfood closure build (turbo run build --filter='@objectstack/dogfood^...', VERDICT 0):

  • pnpm --filter @objectstack/metadata-protocol test: 337 files passed, 7 skipped; 22786 tests passed, 36 skipped. Typecheck exit 0 for metadata-protocol, objectql and runtime.
  • pnpm --filter @objectstack/objectql test: 589 files passed; 12626 tests passed (the three rewritten fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 pins and the six disabled-holder hydration pins included).
  • pnpm --filter @objectstack/runtime exec vitest run src/action-package-addressing.test.ts: 67/67 (main's metadata: in-process bare action reads disagree with the protocol on package-less holders, and a bare removal beside one package takes every slot (ADR-0150 D3/D4, follow-up of #32365) #32560 cases beside this card's row 11 pin).
  • pnpm lint: exit 0. node scripts/pm/dispatch-gates.mjs --commands: 92 derived, each exit 0 after pnpm --filter @objectstack/spec build; --ran: 92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN.
  • S1, measured before its fix: the four service-copy pins failed against the layered read without declinedCopyReplaced and pass with it. Hydrator ablation (on 55a9c1f24; the hydrator is unchanged since): 9 failed / 202 passed with both hunks put back, 211/211 restored.
  • Dogfood under the verify lock: 455 files passed, 3 skipped; 3817 tests passed, 42 skipped; VERDICT command-exit 0.
  • CI on 533a4f932: 56 check runs success, 11 skipped, none failed; mergeable clean.
  • 103d267b7 on top changes text only: the changeset (row 13 and the disabled-holder narrowing on the Clause-② line) and the hydration pin's title and comment (git diff --stat 533a4f932 103d267b7: 2 files changed, 10 insertions(+), 5 deletions(-)). package-scoped-exact-qualifier.test.ts 217/217 and both changeset gates exit 0 on it.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (one migration entry).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is run on the final head.
  • 4. One issue, one branch, one pull request (the branch continues the previous holder's, merged with main).
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: one entry file; the registry, spec-changes.json and the upgrade guide are generated, git-ignored projections.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgV84zNSi6HwSHmMU2c

…, not per deployment (ADR-0150 child B, part 1 of the webhook family)

WIP: the switch, the platform-requested auth.* names, the bundle-addition
rule, the package-scoped /meta read and the send's holder-filtered bundle.

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…latform-requested auth names and the bundle-addition rule

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…nd a pair's member as the package's own

A read naming a dependent whose only member of a bundled name is an addition
(a locale its base does not ship) was refused not-held-by-qualifier once
email_template joined the package-scoped readers, and a pair's added member
on its base's address was replaced by the registry's item. The qualifier now
asks the holders as the caller it names, and keeps an item of a pair of the
admitted package.

The co-owner dogfood case asserts the ADR-0146 D2 refusal a co-owner meets
first at install-local.

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…he email template's narrowing; changeset

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…s base's address for a bundled type only

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…rowing; pin the qualifier's holder ask for the nine

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…mplate send resolves its name from the caller's package (ADR-0150 D3)

The flow notify node records the package whose own flow body is armed
beside its template reference; the email and inbox channels hand it to
sendTemplate/renderTemplate as callerPackageId; the registry loader
resolves the name from it and, for a name two packages hold, reads the
resolved holder's own address. A send that names no package keeps the
bare-name meaning.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…is refused on both exact reads and listed by the upgrade report

Two packages of one App share the App's customization pair, so a locale
row there with no overrides declaration cannot say whose template it
extends: each holder's exact read refuses it naming every holder, the
list scoped to a holder leaves it out, and os migrate stored-references
lists it under pairMembers. One holder, the App's own address and a row
that declares its base keep being served. Pins for F1 (loader rungs,
service ladder, notify node, channels, dogfood) and F2; changeset.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…otype.at, which the package's lib target does not declare

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…) for email_template, beside api

Two packages of two namespaces may now each hold one email template name, so
the second package's item no longer overwrites the first's in MetadataManager's
in-memory store: a read naming a package answers that package's own, and a read
naming none of a two-holder name is the D3 refusal (#31527 B-shape item 1).
Pins that email_template has no metadata redactor, so a save never takes the
carry-forward's code-layer read for it (item 4).

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/objectql/src/package-scoped-e8-readers.test.ts
#	packages/objectql/src/package-scoped-read.ts
…-3giljf

# Conflicts:
#	packages/cli/src/utils/stored-package-references.ts
#	packages/metadata/src/package-slotted-store.ts
…es hold is no holder's own

A package-less row (the environment's item, or a sys_metadata row bound to
no package) of a name two or more packages hold is the customization of one
holder and cannot say which (ADR-0150 D4). Each holder's exact read now
refuses it 409 ambiguous-in-closure naming every holder, the list scoped to a
holder (which a send made from it reads) leaves it out, and
os migrate stored-references lists it under packageLessRows. One holder,
the _env address and the platform-requested auth mails are unchanged. The
rule sits in the exact qualifier, so it covers every type read by package
(epic ruling 6761802819 on #31699).

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/objectql/src/package-scoped-e8-readers.test.ts
#	packages/objectql/src/package-scoped-read.ts
…eview round on #32214

- notify node: a literal payload `template` string records the armed package
  too, as the channels read it (F-A).
- the pair-row owner check is one self-contained function,
  pairRowOnAddress, which the exact read and the scoped list both ask: the
  declared base first (F-B), the App's own pair, two owners refused, a sole
  owner that is not one of the address's bases not served (S2).
- SchemaRegistry.getPackageScopedItem skips a pair row that declares
  another holder as its base, so the App's address answers its own item.
- stored-references hints, its module doc and the changeset say truthfully
  that no door binds an added member to one base today (#28777) (F-C).
- changeset: Metadata-protocol narrowed; slotted set lists api, agent, skill
  and email_template; the package-less rule names PACKAGE_SCOPED_READ_TYPES;
  the pair row's sole-owner and App-slot rules.
- email-service docs page: the new callerPackageId member.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/cli/src/commands/migrate/stored-references.integration.test.ts
#	packages/metadata/src/package-slotted-store.ts
#	packages/objectql/src/bare-name-one-holder.ts
#	packages/objectql/src/registry-bare-name-one-holder-ai-family.test.ts
#	packages/objectql/src/registry.ts
#	packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts
#	packages/services/service-automation/src/engine.ts
…-3giljf

# Conflicts:
#	packages/metadata-protocol/src/protocol-exact-qualifier.ts
#	packages/objectql/src/bare-name-one-holder.ts
…ead an email template by package, as SchemaRegistry does

With email_template narrowed to the namespace, #32277 refuses a bare read
of it on a registry that says it reads the type through getItem
(`narrowed`, 501). SchemaRegistry reads it by package since this change,
so the four doubles that bare-read an email template say so and resolve
it through their own item.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/metadata-protocol/src/index.ts
#	packages/metadata-protocol/src/protocol-exact-qualifier.ts
#	packages/objectql/src/bare-name-one-holder.ts
#	packages/objectql/src/package-scoped-exact-qualifier.test.ts
…ed text

The doc authoring guard refuses an internal issue id in a runtime string;
the reference moves to an adjacent comment.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/metadata/src/package-slotted-store.ts
#	packages/objectql/src/package-scoped-e8-readers.test.ts
#	packages/objectql/src/package-scoped-exact-qualifier.test.ts
#	packages/objectql/src/package-scoped-read.ts
…age reaches the package-less owner check

`''` and the provenance sentinel `sys_metadata` count as no package before
the owner check, so a package-less row the registry answers for a name two
packages hold is refused at both exact addresses (contract review R2-A).
Pins the exact read and the list of a member the holder itself ships beside
such a row (R2-B); the changeset states the mail consequence and the
review's prose nits.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…t, and the exact read of a member the holder ships refuses beside it (R3-A)

The fixture wrote the canonical member's package-less row as
bundle_member 'en-US'; the store spells the canonical member NULL
(overlayMemberValue), so the read never found it and the pin asserted
the registry's answer. Written as stored, ?package=A for a member A
ships answers 409 ambiguous-in-closure naming both holders, while A's
list and the send's read serve A's own (epic ruling 6792134290).

Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
Co-authored-by: Claude <noreply@anthropic.com>
…er of every slot (R3-B)

With email_template in PACKAGE_SLOTTED_STORE_TYPES, DatabaseLoader's
loadMany / loadManyKeyed collapsed to one row per name, dropping every
other stored locale of a bundle and every other package's bundle, where
main answered every row. A bundled type now keeps one row per
(package, name, member), the canonical member spelled null as the store's
bundle_member column spells it; MemoryLoader answers every slot of a
bundled type alike. A rollback by name of a one-slot name restores the
row of the member its body declares (epic ruling 6796068023).

Claude-Session: https://claude.ai/code/session_01QSeKYm8sWjp2Pk76warN7w
Co-authored-by: Claude <noreply@anthropic.com>
…read's refusal, since a dataset declines a package-less row at each holder's address

#32804's two 503 cases rigged "a holder whose own exact-qualifier read
refuses" with a package-less dataset row beside both holders. Under epic
ruling 6817768147 (#32568) a sealed type declines that row at every exact
address, so the read serves the holder's own and the rig no longer refuses
(merge queue: 404 and 200 where 503 was pinned). The cases now throw that
read's former refusal (ambiguous-in-closure naming both holders) from the
holder's exact read, and a new pin holds the declined row: ?package= runs
the holder's own.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgV84zNSi6HwSHmMU2c
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgVaKJ9TPfXPRhdqx3e

Contract review

Served-tier: CONTRACT_REVIEW_TIER (isolated sub-agent, delta review after merge-queue ejection, run 43312543280)
Head-sha: 023ace703c2b6cb3774d619e2e3cca4f4794098c
Local-runs: none
Read-only commands used: git fetch; git diff 103d267b7 023ace703, git diff 176315702 023ace703, git diff 5c0f11349 023ace703 --stat, git diff 2f80f2897 5c0f11349 over packages/metadata-protocol, objectql/registry.ts, metadata, rest. No test run here (workspace has no node_modules); the thread's figures (rest 407 files / 8131 tests, tsc 0, lint 0) are its own and not reproduced. CI on this head was still in progress when read (10 success, 16 pending, 0 failed).
Scope: delta 103d267 → 023ace7 only. 1763157 is a merge of main 5c0f113 with no text conflict: the PR's five product files and both changesets are byte-identical to the accepted head (git diff 103d267b7 176315702 over them is empty). 023ace7 touches one file, packages/rest/src/analytics-routes.test.ts (+50 −11), test-only. PR net effect vs main is the accepted 13 files plus that test file.
① Rewritten #32804 tests keep the 503 meaning: yes. Both ("a holder whose own exact-qualifier read refuses…", "?package= naming a holder whose own address refuses…") keep every assertion (503 SERVICE_UNAVAILABLE, neither holder named, queryDataset not called) over the same matrix (three admission sets × bare / ?callerPackage= / ?package=C, and × ?package=A|B). Only the fixture moved: exactReadsRefuse(A, B) wraps the real protocol and throws a PackageScopedNameRefusedError (ambiguous-in-closure, candidates A and B, packages only) from getMetaItem when packageId is a holder; every other read goes to the real protocol and registry. Traced through analytics-routes.ts / analytics-dataset-admission-order.ts: the bare, caller and ?package=C arms still get the real refusal from the by-name read, then each per-holder read refuses → unresolved, none admitted → undetermined → 503; at ?package=holder the top-level read itself refuses naming packages only (so not the rethrow arm), then the same. That is the door rule #32804 pinned (a per-holder refusal stays inside the arm and is never answered), and the technique is the one #32804's own neighbouring test ("a per-holder read's refusal of any reason…") already uses. #32804's contract is not weakened; no #32804 product line is touched.
② New pin is correct per ruling 6817768147: a package-less dataset row beside holders A and B, ?package=A → 200 running alpha_item, ?package=B → 200 running beta_item, one queryDataset call each. Dataset is a sealed type, so the row is declined at each holder's exact address and the holder's own item is served.
③ No other semantic clash found in the merge. Main since base 2f80f28 touched metadata-protocol only for #31698 (permission-set name is per namespace: save-door refusal, projectorEventPackage on five write doors); none of it reads an exact qualifier or a package-less row, and none of the PR's five files changed. objectql/registry.ts changes are the security-catalog claim set (permission only). #32521 (metadata boot priming) states that a package-less row beside two holders stays the environment's own item and a read naming either holder answers that holder's own body, which agrees with this PR. #32804's door reads holders through getMetaItem at the exact qualifier, which is the clash already found and repaired. Limit: this is by reading; the metadata-protocol, objectql and runtime suites on the merged head are shown only by CI, which had not finished.
④ Changeset / PR body: no line is wrong, one line is worth adding (nit). The class statement and the FROM → TO list already cover dataset as a sealed type, and "No first-party composition has two packages holding one name with a package-less row" still holds. Missing is the cross-PR effect: on POST /analytics/dataset/query (and the batch door), a package-less dataset row beside two holders no longer makes each holder's exact read refuse, so #32804's "every holder's own read refuses → 503 for every caller" row no longer fires for that composition: the bare / ?callerPackage= / non-holder ?package= arms go from 503 to the admission-judged answer (404, 200 or 409 naming the admitted holders), and ?package=<holder> goes from 503 to that holder's own dataset. The Clause-② widening sentence names action (and agent, skill, app); adding dataset there with this door named would close it. No @objectstack/rest changeset is needed (no rest product code changed).
Nits (none blocking):

  • ④ above: one line in the changeset's Clause-② widening and the PR body for the dataset door's moved answer.
  • The new pin covers only ?package=<holder> with both objects admitted. The arms main pinned at 503 with the row present (bare, ?callerPackage=, ?package=C) now have no pin with the row present; one case (for example bare name, admitted to alpha only → 200 alpha, row never run) would hold the door's new answer.
  • The helper's thrown refusal carries callerPackageId: null; the product's own throw passes the address package. The door does not read it, so nothing depends on it.
    Implemented-by: card thread session_01L6bhgV84zNSi6HwSHmMU2c
    Reviewed-by: contract reviewer, fable
    VERDICT: PASS-with-nits

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgVaKJ9TPfXPRhdqx3e

Flake judgement · 2026-11-12T22:38Z: Test Core (8/8) on head 023ace703c2b6cb3774d619e2e3cca4f4794098c (run 43318020420, job 131532249798) failed at log line 217: the step "Fetch this commit's history and the base branch" timed out after 20 minutes. The job ran no test, so it says nothing about this PR. The seat re-runs the failed job once; a second failure is treated as real.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-sales · session_01L6bhgVaKJ9TPfXPRhdqx3e

维护者速读

席位核对

  • Head: 023ace703c2b6cb3774d619e2e3cca4f4794098c; 11 behind main (under the K bar), mergeable_state clean. It is the accepted head 103d267b7 plus a merge of main 5c0f11349 (1763157, no text conflict; the PR's product files and changesets byte-identical) and the test-only commit 023ace7.
  • Shape: 14 files, +725 / −89, under 3000 lines, no Tier H path; the declaration lines are unchanged from ACCEPT 6836671040; the body (r9) adds the dataset query door's cross-PR answer change, with measurements on this head.
  • Gates: check-runs on this head, 60 success, 15 skipped, none failing or pending. Test Core (8/8) first failed on an infrastructure timeout (git history fetch, no test ran; flake judgement 6837872604), re-run once and passed. Card thread: rest 407 files / 8131 tests, tsc 0, lint 0. Product code unchanged since the dogfood run on 533a4f93 (3817 tests, VERDICT 0).
  • Contract review: delta PASS-with-nits 6837488300 at this head (test-only delta; the fix(rest): the dataset query door judges admission on the refusal arm before it names holders (ADR-0150 D3 after ADR-0149 D2) #32804 contract kept; no other semantic clash from main).
  • Accepted deviations: the delta record's nits: the changeset's Clause-② widening sentence does not name dataset (the body r9 carries it); the bare arm with the row present is not pinned; the test helper passes callerPackageId: null. No extra round per the maintainer's rule.
  • Landing: epic design(metadata): address behavioural metadata by package — key flows, actions, jobs and other non-object types by (package, name); re-open ADR-0028 (replaces #30806 phase 3 renames) #31527 card, on the release line. Re-arms auto-merge after the merge-queue ejection.

ACCEPT

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit a888c5c Nov 13, 2026
116 of 118 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-pjmygu branch November 13, 2026 00:01
os-sales pushed a commit that referenced this pull request Nov 13, 2026
…older's exact and layered reads, as main's #32741 rules for a sealed type

The merge of main brought #32741: a package-less row at a holder's exact
address is declined for a sealed type, on every read and in every
hydration state. Seven of this PR's webhook pins went red on the merged
tree:

- two holders: the plain exact reads of the environment's item (three
  stamps) and the layered read of a package-less stored row answered 409
  ambiguous-in-closure here; each now answers the holder's own
  declaration, the package-less item declined, nothing picked between the
  holders. That FROM was never released (two holders are refused at
  install on main).
- one holder: the exact read of the environment's item, the layered
  read's overlay, and the boot hydration of a package-less row. Measured
  on the merged tree with `webhook` out of the three type sets (core,
  metadata and objectql dists rebuilt, each set read back without it):
  the same declines, so they are #32741's own answers, not this PR's,
  and no Clause-② entry is added.

The pins now pin those answers, each title naming its FROM. The changeset
drops the exact/layered 409 it no longer answers, and says the boot
hydration declines the package-less row too, while a row naming a package
that does not declare the name still grafts onto the first-registered
holder and of two holder-named rows only the last loaded applies (#32692).

No source change.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgUJAvdCcvoqdsFGVz8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants